/*
 * File    : FMFileManagerImpl.java
 * Created : 12-Feb-2004
 * By      : parg
 * 
 * Azureus - a Java Bittorrent client
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package com.aelitis.azureus.core.diskmanager.file.impl;

/**
 * @author parg
 *
 */

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.util.AEDiagnostics;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AEThread;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.IndentWriter;

import com.aelitis.azureus.core.diskmanager.file.FMFile;
import com.aelitis.azureus.core.diskmanager.file.FMFileManager;
import com.aelitis.azureus.core.diskmanager.file.FMFileManagerException;
import com.aelitis.azureus.core.diskmanager.file.FMFileOwner;
import com.aelitis.azureus.core.util.LinkFileMap;

public class FMFileManagerImpl implements FMFileManager {
    public static final boolean DEBUG = false;

    protected static FMFileManagerImpl singleton;
    protected static AEMonitor class_mon = new AEMonitor("FMFileManager:class");

    public static FMFileManager getSingleton() {
        try {
            class_mon.enter();

            if (singleton == null) {

                singleton = new FMFileManagerImpl();
            }

            return (singleton);

        } finally {

            class_mon.exit();
        }
    }

    protected LinkedHashMap map;
    protected AEMonitor map_mon = new AEMonitor("FMFileManager:Map");

    protected HashMap<Object, LinkFileMap> links = new HashMap<Object, LinkFileMap>();
    protected AEMonitor links_mon = new AEMonitor("FMFileManager:Links");

    protected boolean limited;
    protected int limit_size;

    protected AESemaphore close_queue_sem;
    protected List close_queue;
    protected AEMonitor close_queue_mon = new AEMonitor("FMFileManager:CQ");

    protected List files;
    protected AEMonitor files_mon = new AEMonitor("FMFileManager:File");

    protected FMFileManagerImpl() {
        limit_size = COConfigurationManager.getIntParameter("File Max Open");

        limited = limit_size > 0;

        if (DEBUG) {

            System.out.println("FMFileManager::init: limit = " + limit_size);

            files = new ArrayList();
        }

        map = new LinkedHashMap(limit_size, (float) 0.75, true); // ACCESS order selected - this means oldest

        if (limited) {

            close_queue_sem = new AESemaphore("FMFileManager::closeqsem");

            close_queue = new LinkedList();

            Thread t = new AEThread("FMFileManager::closeQueueDispatcher") {
                public void runSupport() {
                    closeQueueDispatch();
                }
            };

            t.setDaemon(true);

            t.start();
        }
    }

    protected LinkFileMap getLinksEntry(TOTorrent torrent) {
        Object links_key;

        try {

            links_key = torrent.getHashWrapper();

        } catch (Throwable e) {

            Debug.printStackTrace(e);

            links_key = "";
        }

        LinkFileMap links_entry = links.get(links_key);

        if (links_entry == null) {

            links_entry = new LinkFileMap();

            links.put(links_key, links_entry);
        }

        return (links_entry);
    }

    public void setFileLinks(TOTorrent torrent, LinkFileMap new_links) {
        try {
            links_mon.enter();

            LinkFileMap links_entry = getLinksEntry(torrent);

            Iterator<LinkFileMap.Entry> it = new_links.entryIterator();

            while (it.hasNext()) {

                LinkFileMap.Entry entry = it.next();

                int index = entry.getIndex();

                File source = entry.getFromFile();
                File target = entry.getToFile();

                // System.out.println( "setLink:" + source + " -> " + target );

                if (target != null && !source.equals(target)) {

                    if (index >= 0) {

                        links_entry.put(index, source, target);

                    } else {

                        links_entry.putMigration(source, target);
                    }
                } else {

                    links_entry.remove(index, source);
                }
            }
        } finally {

            links_mon.exit();
        }
    }

    public File getFileLink(TOTorrent torrent, int file_index, File file) {
        // this function works on the currently defined links and will only accept
        // them as valid if their 'from' location matches the 'file' being queried.
        // if not the origial file is returned, NOT null

        // These semantics are important during file-move operations as the move-file
        // logic does not update links until AFTER the move is complete. If we don't
        // verify the 'from' path in this case then teh old existing linkage overrides
        // the new destination during the move process and causes it to fail with
        // 'file already exists'. There is possibly an argument that we shouldn't
        // take links into account when moving

        try {
            links_mon.enter();

            LinkFileMap links_entry = getLinksEntry(torrent);

            LinkFileMap.Entry entry = links_entry.getEntry(file_index, file);

            File res = null;

            if (entry == null) {

                res = file;

            } else {

                if (file.equals(entry.getFromFile())) {

                    res = entry.getToFile();

                } else {

                    res = file;
                }
            }

            // System.out.println( "getLink:" + file + " -> " + res );

            return (res);

        } finally {

            links_mon.exit();
        }
    }

    public FMFile createFile(FMFileOwner owner, File file, int type)

    throws FMFileManagerException {
        FMFile res;

        if (AEDiagnostics.USE_DUMMY_FILE_DATA) {

            res = new FMFileTestImpl(owner, this, file, type);

        } else {

            if (limited) {

                res = new FMFileLimited(owner, this, file, type);

            } else {

                res = new FMFileUnlimited(owner, this, file, type);
            }
        }

        if (DEBUG) {

            try {
                files_mon.enter();

                files.add(res);

            } finally {

                files_mon.exit();
            }
        }

        return (res);
    }

    protected void getSlot(FMFileLimited file) {
        // must close the oldest file outside sync block else we'll get possible deadlock

        FMFileLimited oldest_file = null;

        try {
            map_mon.enter();

            if (DEBUG) {
                System.out.println("FMFileManager::getSlot: " + file.getName() + ", map_size = " + map.size());
            }

            if (map.size() >= limit_size) {

                Iterator it = map.keySet().iterator();

                oldest_file = (FMFileLimited) it.next();

                it.remove();
            }

            map.put(file, file);

        } finally {

            map_mon.exit();
        }

        if (oldest_file != null) {

            closeFile(oldest_file);

        }
    }

    protected void releaseSlot(FMFileLimited file) {
        if (DEBUG) {
            System.out.println("FMFileManager::releaseSlot: " + file.getName());
        }

        try {
            map_mon.enter();

            map.remove(file);

        } finally {

            map_mon.exit();
        }
    }

    protected void usedSlot(FMFileLimited file) {
        if (DEBUG) {
            System.out.println("FMFileManager::usedSlot: " + file.getName());
        }

        try {
            map_mon.enter();

            // only update if already present - might not be due to delay in
            // closing files

            if (map.containsKey(file)) {

                map.put(file, file); // update MRU
            }
        } finally {

            map_mon.exit();
        }
    }

    protected void closeFile(FMFileLimited file) {
        if (DEBUG) {
            System.out.println("FMFileManager::closeFile: " + file.getName());
        }

        try {
            close_queue_mon.enter();

            close_queue.add(file);

        } finally {

            close_queue_mon.exit();
        }

        close_queue_sem.release();
    }

    protected void closeQueueDispatch() {
        while (true) {

            if (DEBUG) {

                close_queue_sem.reserve(1000);

            } else {

                close_queue_sem.reserve();
            }

            FMFileLimited file = null;

            try {
                close_queue_mon.enter();

                if (close_queue.size() > 0) {

                    file = (FMFileLimited) close_queue.remove(0);

                    if (DEBUG) {

                        System.out.println("FMFileManager::closeQ: " + file.getName() + ", rem = " + close_queue.size());
                    }
                }
            } finally {

                close_queue_mon.exit();
            }

            if (file != null) {

                try {
                    file.close(false);

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }

            if (DEBUG) {

                try {
                    files_mon.enter();

                    int open = 0;

                    for (int i = 0; i < files.size(); i++) {

                        FMFileLimited f = (FMFileLimited) files.get(i);

                        if (f.isOpen()) {

                            open++;
                        }
                    }

                    System.out.println("INFO: files = " + files.size() + ", open = " + open);

                } finally {

                    files_mon.exit();
                }
            }
        }
    }

    protected void generate(IndentWriter writer) {
        writer.println("FMFileManager slots");

        try {
            writer.indent();

            try {
                map_mon.enter();

                Iterator it = map.keySet().iterator();

                while (it.hasNext()) {

                    FMFileLimited file = (FMFileLimited) it.next();

                    writer.println(file.getString());
                }

            } finally {

                map_mon.exit();
            }
        } finally {

            writer.exdent();
        }
    }

    protected static void generateEvidence(IndentWriter writer) {
        getSingleton();

        singleton.generate(writer);
    }
}
